
#include "can.hxx"
#include "http.hxx"
#include "sock-inet-tcp.hxx"

#include <cstdlib>
#include <dirent.h>
#include <fstream>
#include <functional>
#include <iostream>
#include <list>
#include <map>
#include <vector>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <thread>

#include "unistd.h"

using std::ios;
using std::cerr;
using std::cin;
using std::cout;
using std::endl;
using std::exception;
using std::function;
using std::ifstream;
using std::istringstream;
using std::list;
using std::map;
using std::ofstream;
using std::ostringstream;
using std::ref;
using std::remove;
using std::runtime_error;
using std::set;
using std::string;
using std::thread;
using std::vector;

////////////////////////////////////////////////////////////////////////////////

template <typename T> string print(const T& t) {
    ostringstream os;
    os << t;
    return os.str();
}

template <> string print<vector<char>>(const vector<char>& t) {
    return string(t.begin(), t.end());
}

template <typename T> T& operator>>(const string& s, T& t) {
    istringstream(s) >> t;
    return t;
}

template <typename T> T& operator>>(const vector<char>& v, T& t) {
    return (print(v) >> t);
}

////////////////////////////////////////////////////////////////////////////////

int main(int argc, const char* argv[]) {
    string host = "localhost";
    string service = "2611";

    switch(argc)
    {
    case 4:
        service = argv[3];
        /* no break */

    case 3:
        host = argv[2];
        /* no break */

    case 2:
        if(chdir(argv[1]) < 0) /* change working path to requested dir */
        {
            perror(argv[1]);
            exit(EXIT_FAILURE);
        }
        break;

    default:
        fprintf(stderr, "usage: %s dir [hostname [port]]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    try {
        auto local_server = sock_inet_tcp::listen(sock_inet::address(0, 0));
        auto remote_server = sock_inet_tcp::connect(sock_inet::address(host, service));
        map<string,function<void(const vector<string>&)>> commands;
        string crlf = "\r\n";

        commands["connect"] = [&](const vector<string>& args) {
            remote_server <<
                "GET / HTTP/1.1" << crlf <<
                "Connection: Keep-Alive" << crlf <<
                "Content-Type: application/json" << crlf <<
                "Content-Length: " << print(local_server.get_local().get_service()).length() << crlf <<
                "" << crlf <<
                print(local_server.get_local().get_service()) <<
                ostream::flush;

            if(http::read_response(remote_server).compare("200 OK") != 0) {
                throw http::bad_response_error(); // TODO:
            }

            cout << print(http::read_body(remote_server, http::read_headers(remote_server))) << endl;
        };

        commands["publish"] = [&](const vector<string>& args) {
            DIR* dir = opendir(".");

            if(dir == NULL) {
                throw runtime_error("opendir");
            }

            string files;

            for(dirent* entry; (entry = readdir(dir)) != NULL; ) {
                if(entry->d_type == DT_REG)
                {
                    files += entry->d_name;
                    files += crlf;
                }
            }

            free(dir);

            remote_server <<
                "GET /put-files HTTP/1.1" << crlf <<
                "Connection: Keep-Alive" << crlf <<
                "Content-Type: application/json" << crlf <<
                "Content-Length: " << files.length() << crlf <<
                "" << crlf <<
                files <<
                ostream::flush;

            if(http::read_response(remote_server).compare("200 OK") != 0) {
                throw http::bad_response_error(); // TODO:
            }

            http::skip_body(remote_server, http::read_headers(remote_server));
        };

        commands["dir"] = [&](const vector<string>& args) {
            remote_server <<
                "GET /get-files HTTP/1.1" << crlf <<
                "Connection: Keep-Alive" << crlf <<
                "Content-Type: application/json" << crlf <<
                "" << crlf <<
                ostream::flush;

            if(http::read_response(remote_server).compare("200 OK") != 0) {
                throw http::bad_response_error(); // TODO:
            }

            istringstream is(print(http::read_body(remote_server, http::read_headers(remote_server))));

            for(string file; is >> file; cout << file << endl);
        };

        commands["get"] = [&](const vector<string>& args) {
            remote_server <<
                "GET /get-owners HTTP/1.1" << crlf <<
                "Connection: Keep-Alive" << crlf <<
                "Content-Type: application/json" << crlf <<
                "Content-Length: " << args[0].length() << crlf <<
                "" << crlf <<
                args[0] <<
                ostream::flush;

            if(http::read_response(remote_server).compare("200 OK") != 0) {
                throw http::bad_response_error(); // TODO:
            }

            istringstream is(print(http::read_body(remote_server, http::read_headers(remote_server))));

            for(string owner; is >> owner; ) {
                auto delimiter = owner.find(":");

                if(delimiter == string::npos) {
                    throw http::bad_response_error(); // TODO:
                }

                try {
                    auto client = sock_inet_tcp::connect(sock_inet::address(owner.substr(0, delimiter), owner.substr(delimiter + 1)));

                    client <<
                        "GET / HTTP/1.1" << crlf <<
                        "Connection: close" << crlf <<
                        "Content-Type: application/json" << crlf <<
                        "Content-Length: " << args[0].length() << crlf <<
                        "" << crlf <<
                        args[0] <<
                        ostream::flush;

                    if(http::read_response(client).compare("200 OK") != 0) {
                        throw http::bad_response_error(); // TODO:
                    }

                    auto headers = http::read_headers(client);
                    auto length = headers.find("Content-Length");

                    if(length == headers.end()) {
                        throw http::length_required_error(); // TODO:
                    }

                    auto size = atoi(length->second.c_str());

                    ofstream fs(args[0], ios::binary);

                    vector<char> buffer(4096);

                    while(size > 0) {
                        if(size < (int)buffer.size()) {
                            buffer.resize(size);
                        }

                        client >> buffer;
                        fs.write(buffer.data(), (long)buffer.size());

                        size -= buffer.size();
                    }

                    cout << "transfer complete" << endl;

                    return;
                }
                catch(exception&) {
                }
            }

            remove(args[0].c_str());
            cerr << "transfer failed" << endl;
        };

        thread server_thread([&]() {
            list<thread> threads;
            list<sock_inet_tcp> clients;

            while(true) {
                try {
                    clients.emplace_back(local_server.accept());

                    threads.push_back(thread([&](sock_inet_tcp& client) {

                        while(true) {
                            try {
                                try {
                                    auto location = http::read_request(client);

                                    if(location.compare("/") != 0) {
                                        throw http::not_found_error();
                                    }

                                    string file;

                                    if((http::read_body(client, http::read_headers(client)) >> file).empty()) {
                                        throw http::bad_request_error();
                                    }

                                    ifstream fs(file, ios::binary | ios::ate);

                                    if(!fs) {
                                        throw http::bad_request_error();
                                    }

                                    client <<
                                        "HTTP/1.1 200 OK" << crlf <<
                                        "Connection: close" << crlf <<
                                        "Content-Type: application/force-download" << crlf <<
                                        "Content-Length: " << fs.tellg() << crlf <<
                                        "" << crlf;

                                    fs.seekg(0, ios::beg);

                                    vector<char> buffer(4096);

                                    while(!fs.eof()) {
                                        fs.read(buffer.data(), (long)buffer.size());

                                        if(fs.gcount() < (long)buffer.size()) {
                                            buffer.resize(fs.gcount());
                                        }

                                        client << buffer;
                                    };

                                    client << ostream::flush;

                                    break;
                                }
                                catch(http::error& e) {
                                    client << e.what() << crlf << crlf;
                                    throw stream_closed_exception();
                                }
                            }
                            catch(stream_closed_exception& e) {
                                break;
                            }
                        }
                    }, ref(clients.back())));
                }
                catch(exception& e) {
                    clients.clear();

                    for(auto& thread : threads) {
                        thread.join();
                    }

                    break;
                }
            }
        });

        try {
            commands["connect"]({ });
            commands["publish"]({ });

            cout << "list of local files was sent to the server" << endl;

            while(true) {
                string command;
                cin >> command;

                if(command.compare("DIR") == 0) {
                    commands["dir"]({ });
                }
                else if(command.compare("GET") == 0) {
                    string file;
                    cin >> file;

                    if(!ifstream(file)) {
                        commands["get"]({ file });
                        commands["publish"]({ });
                    }
                    else {
                        cerr << file << ": file already exists" << endl;
                    }
                }
                else if(command.compare("QUIT") == 0) {
                    throw exception();
                }
            }
        }
        catch(exception& e) {
            local_server.shutdown();
            server_thread.join();
        }
    }
    catch(exception& e) {
        cerr << e.what() << endl;
    }

    return 0;
}

////////////////////////////////////////////////////////////////////////////////
